package fr.cephb.lindenb.operon;

import java.io.*;
import java.util.*;


import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;


import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;


import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.context.Context;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;

import  fr.cephb.lindenb.operon.XMLUtilities;

class OPERON
	{
	public static final String NS="http://operon.cng.fr";
	}

/**
 * 
 * AbstractCodeGenerator
 *
 */
public class CodeGenerator
	{
	
	protected  class NodeWrapper
		{
		Element element;
		public NodeWrapper(Element element)
			{
			this.element = element;
			}
		public Element getElement() { return this.element;}
		
		
		public String getAttribute(String name,String defaultValue)
			{
			Attr att= getElement().getAttributeNode(name);
			if(att==null) return defaultValue;
			return att.getValue();
			}
		
		public String getAttribute(String name)
			{
			Attr att= getElement().getAttributeNode(name);
			if(att==null) throw new IllegalArgumentException("No such attribute: "+name);
			return att.getValue();
			}
		}
		
	public class DatabaseRecord
		extends NodeWrapper
		{
		Structure structure;
		Vector<Field> fields=new Vector<Field>();
		public DatabaseRecord(Element root,Structure structure)
			throws IOException
			{
			super(root);
			this.structure=structure;
			for(Element e: XMLUtilities.elements(root, OPERON.NS, "column-ref"))
				{
				Attr att= e.getAttributeNode("ref");
				if(att==null) throw new IOException("@ref missing");
				Field f= this.structure.name2field.get(att.getValue());
				if(f==null)  throw new IOException("cannot find element "+att.getValue());
				this.fields.addElement(f);
				}
			}
		
		public Collection<SimpleField> getSimpleFields()
			{
			ArrayList<SimpleField> sf= new ArrayList<SimpleField>();
			for(Field f: this.fields)
				{
				if(f instanceof SimpleField) sf.add(SimpleField.class.cast(f));
				}
			
			return sf;
			}
		}
	
	public class DatabaseInstance
	extends NodeWrapper
		{
		Structure structure;
		public DatabaseInstance(Element root,DefinitionFile df)
			throws IOException
			{
			super(root);
			Element e = XMLUtilities.firstChild(root, OPERON.NS, "table-ref");
			if(e==null)  throw new IOException("cannot find element table-ref");
			Attr att= e.getAttributeNode("ref");
			if(att==null) throw new IOException("@ref missing");
			this.structure=df.findStructureByName(att.getValue());
			if(this.structure==null)  throw new IOException("cannot find element "+att.getValue());
			}
		public Structure getStructure()
			{
			return this.structure;
			}
		
		public String getName()
			{
			return getAttribute("name");
			}
		public String getXmlName()
			{
			return getName();
			}
	
		public String getMethod()
			{
			return getAttribute("method","DB_BTREE").trim();
			}
		public String getFilename()
			{
			String name=getAttribute("file",null);
			return name==null?getStructure().getName()+".db":name;
			}
		public String getPrefix()
			{
			return getAttribute("prefix",getName()+"_");
			}
		
		public String getTypedef()
			{
			return getAttribute("typedef",getName()+"_t");
			}
		
		public String getPtr()
			{
			return getAttribute("ptr",getName()+"Ptr");
			}
		}
	
	/**
	 *
	 * Operon
	 *
	 */
	public class Operon
		extends NodeWrapper
		{
		Vector<DatabaseInstance> databases= new Vector<DatabaseInstance>();
		
		public Operon(Element root,DefinitionFile df)
			throws IOException
			{
			super(root);
			for(Element e: XMLUtilities.elements(root, OPERON.NS, "database-ref"))
				{
				Attr att= e.getAttributeNode("ref");
				if(att==null) throw new IOException("@ref missing");
				DatabaseInstance db=df.findDatabaseByName(att.getValue());
				if(db==null)  throw new IOException("cannot find db @"+att.getValue());
				databases.addElement(db);
				}
			}
			
		public String getName()
			{
			return getAttribute("name");
			}
			
	


				
		public Collection<DatabaseInstance> getDatabases()
			{
			return this.databases;
			}
		public String getPrefix()
			{
			return getAttribute("prefix",getName()+"_");
			}
		
		public String getTypedef()
			{
			return getAttribute("typedef",getName()+"_t");
			}
		
		public String getPtr()
			{
			return getAttribute("ptr",getName()+"Ptr");
			}
		public String getDbHome()
			{
			return getAttribute("db-home",null);
			}
			
		public String getOpenMode()
			{
			return getAttribute("open-mode","0");
			}	
		}

	public  class Component
	extends NodeWrapper
		{
		Component(Element e)
			{
			super(e);
			}
		
		public String getName()
			{
			return getAttribute("name");
			}
		public String getXmlName()
			{
			return getName();
			}
		
		public String getDescription()
			{
			Element e=XMLUtilities.firstChild(getElement(), OPERON.NS, "description");
			return e==null?getName():e.getTextContent();
			}
		
		public String  getXmlTagName()
			{
			return getName();
			}
		}
	
	public class DefinitionFile
		extends NodeWrapper
		{
		private File file;
		private Vector<Structure> structures= new Vector<Structure>();
		private Vector<DatabaseInstance> dbInstances= new Vector<DatabaseInstance>();
		private Vector<Operon> operons= new Vector<Operon>();
		DefinitionFile(File file,Element e)
			{
			super(e);
			this.file=file;
			}
		
		public Structure findStructureByName(String name)
			{
			for(Structure structure: getStructures())
				{
				if(structure.getName().equals(name)) return structure;
				}
			return null;
			}
		
		public DatabaseInstance findDatabaseByName(String name)
			{
			for(DatabaseInstance db: getDatabases())
				{
				if(db.getName().equals(name)) return db;
				}
			return null;
			}
		
		public Collection<Structure> getStructures()
			{
			return this.structures;
			}
		
		public Collection<DatabaseInstance> getDatabases()
			{
			return this.dbInstances;
			}
			
		public Collection<Operon> getOperons()
			{
			return this.operons;
			}
		
		public String getName()
			{
			return getAttribute("name");
			}
		}
	
	protected  class Field
		extends Component
		{
		Structure mystructure;
		String name;
		Field(Element e)
			{
			super(e);
			}
		
		
		}
	
	public  class SimpleField
		extends Field
		{
		SimpleField(Element e)
			{
			super(e);
			}
		
		public String getFixedSize()
			{
			return getAttribute("size");
			}


		public boolean isUnique()
			{
			return getAttribute("unique","false").equalsIgnoreCase("true");
			}
		
		public boolean isCaseSensible()
			{
			return getAttribute("case-sensible","true").equalsIgnoreCase("true");
			}

		public boolean isArray()
			{
			return getFixedSize()!=null;
			}	
		
		public boolean isString()
			{
			return getType().equals("char*") ||
					(getType().equals("char") && isArray()) ||
					(getType().equals("fid_t"))
					;
			}
		
		public String getSqlIndex()
			{
			String s= getAttribute("sql-index",null);
			if(s==null) return null;
			if(s.equals("true")) s="BTREE";
			else if(s.equals("false")) return null;
			return s;
			}

		public boolean isSqlIndexed()
			{
			return getSqlIndex()!=null;
			}

		public String getAccess()
			{
			return getAttribute("access","public");
			}
		


		public String getReadAccess()
			{
			return getAttribute("read-access",getAccess());
			}

		
		public String getWriteAccess()
			{
			return getAttribute("write-access",getAccess());
			}

/*
		public String freePtr(String ptrName)
			{
			if(!isPointer()) return "";
			return "Free("+ptrName+"->"+getName()+"); "+ptrName+"->"+getName()+"=NULL;";
			}

		
		public String printXML(String outName, String ptrName)
			{
			if(isString())
				{
				return "xmlputs("+outName+","+ptrName+"->"+getName()+");";
				}
			if(getType().equals("int") || getType().equals("short"))
				{
				return "fprintf("+outName+",\"%d\","+ptrName+"->"+getName()+");";
				}
			if(getType().equals("long int") ||getType().equals("long"))
				{
				return "fprintf("+outName+",\"%ld\","+ptrName+"->"+getName()+");";
				}
			if(getType().equals("char"))
				{
				return "xmlputc("+outName+","+ptrName+"->"+getName()+");";
				}
			throw new RuntimeException("cannot handle"+getType());
			}
		
		public String printJSON(String outName, String ptrName)
			{
			if(isString())
				{
				if(isPointer())
					{
					return "if("+ptrName+"->"+getName()+"==NULL) { fputs(\"null\","+outName+");} else { fprintf("+outName+",\"\\\"%s\\\"\","+ptrName+"->"+getName()+");}";
					}
				else
					{
					return "fprintf("+outName+",\"\\\"%s\\\"\","+ptrName+"->"+getName()+");";
					}
				}
			if(getType().equals("char"))
				{
				return "fprintf("+outName+",\"\\\"%c\\\"\","+ptrName+"->"+getName()+");";
				}
			return printText(outName,ptrName);
			}

		
		public String printText(String outName, String ptrName)
			{
			if(isString())
				{
				if(isPointer())
					{
					return "if("+ptrName+"->"+getName()+"==NULL) { fputs(\"null\","+outName+");} else { fprintf("+outName+",\"%s\","+ptrName+"->"+getName()+");}";
					}
				else
					{
					return "fprintf("+outName+",\"%s\","+ptrName+"->"+getName()+");";
					}
				}
			if(getType().equals("int") || getType().equals("short"))
				{
				return "fprintf("+outName+",\"%d\","+ptrName+"->"+getName()+");";
				}
			if(getType().equals("long int") ||getType().equals("long"))
				{
				return "fprintf("+outName+",\"%ld\","+ptrName+"->"+getName()+");";
				}
			if(getType().equals("char"))
				{
				return "fprintf("+outName+",\"%c\","+ptrName+"->"+getName()+");";
				}
			throw new RuntimeException("cannot handle"+getType());
			}
*/
		
		public String packDBT(String dbtName)
			{
			return packDBT(dbtName,null);
			}
		
		public String packDBT(String dbtName,String prtName)
			{
			String prefix= (prtName==null?"":prtName+"->");
			if(isString())
				{
				if(isPointer())
					{
					return "berkeleyPackString("+dbtName+","+ prefix+getName()+")";
					}
				else if(getType().equals("fid_t"))
					{
					return "berkeleyPackString("+dbtName+","+ prefix+getName()+")";
					}
				else
					{
					return "berkeleyPack("+dbtName+",&("+prefix+getName()+"),"+getFixedSize()+"*sizeof("+getType()+"))";
					}
				}
			return "berkeleyPack("+dbtName+",&("+prefix+getName()+"),sizeof("+getType()+"))";
			}

		public String unpackDBT(String bytes,String prtName,String sizeName)
			{
			String prefix=prtName+"->";
			if(isString())
				{
				if(isPointer())
					{
					return prefix+getName()+"=strdup(((&"+bytes+"["+sizeName+"]))); "+sizeName+"+=1+strlen("+prefix+getName()+");";
					}
				else if(getType().equals("fid_t"))
					{
					return "strncpy("+prefix+getName()+",((&("+bytes+"["+sizeName+"]))),FID_MAX_LENGTH); "+sizeName+"+=1+strlen("+prefix+getName()+");";
					}
				else
					{
					return "strncpy(&("+prefix+getName()+"),((&("+bytes+"["+sizeName+"]))),"+getFixedSize()+"*sizeof(char)); "+sizeName+"+="+getFixedSize()+"*sizeof(char);";
					}
				}
			return "memcpy(&("+prefix+getName()+"),((&("+bytes+"["+sizeName+"]))),sizeof("+getType()+")); "+sizeName+"+=sizeof("+getType()+");";
			}
		
		public String getType()
			{
			return getAttribute("type");
			}
		
		public String getConstType()
			{
			if(getType().endsWith("*"))
				{
				return "const "+getType();
				}
			return getType();
			}
		
		public boolean isPointer()
			{
			return getType().endsWith("*");
			}
		}
	
	public  class Structure
		extends Component
		{
		// fields as their are declared
		private Vector<Field> fields= new Vector<Field>();
		//map name to field
		private HashMap<String, Field> name2field= new HashMap<String, Field>();
		/* key */
		private DatabaseRecord key;
		/** value */
		private DatabaseRecord value;
		
		public Structure(Element e)
			{
			super(e);	
			}
		
		public DatabaseRecord getKey() { return this.key;}
		public DatabaseRecord getValue() { return this.value;}
		
		public String getPrefix()
			{
			return getAttribute("prefix",getName()+"_");
			}
		
		public String getTypedef()
			{
			return getAttribute("typedef",getName()+"_t");
			}
		
		public String getPtr()
			{
			return getAttribute("ptr",getName()+"Ptr");
			}
		
		public String getDbType()
			{
			return getAttribute("db-type",getName()+"DB_TREE");
			}
		
		public String getDbName()
			{
			return getAttribute("db-name",getName()+".db");
			}
		
		public Collection<SimpleField> getSimpleFields()
			{
			ArrayList<SimpleField> sf= new ArrayList<SimpleField>();
			for(Field f: fields)
				{
				if(f instanceof SimpleField) sf.add(SimpleField.class.cast(f));
				}
			return sf;
			}
		
		public SimpleField getFidField()
			{
			for(SimpleField f: getSimpleFields())
				{
				if(f.getType().equals("fid_t")) return f;
				}
			return null;	
			}

		public boolean hasFid()
			{
			return getFidField()!=null;			
			}
		}
	
	protected Structure newStructure(Element table)
		{
		return new Structure(table);
		}
	
	private Structure parseTable(Element table) throws IOException
		{
		Structure structure= newStructure(table);
		Element fields= XMLUtilities.firstChild(table, OPERON.NS, "fields");
		if(fields==null) throw new IOException("op:fields not found");
		for(Node n= fields.getFirstChild();
			n!=null;
			n=n.getNextSibling()
			)
			{
			if(XMLUtilities.isA(n, OPERON.NS, "column"))
				{
				parseField(structure,Element.class.cast(n));
				}
			}
		if(structure.fields.isEmpty())
			{
			throw new IOException("structure is empty");
			}
		
		Element dbt = XMLUtilities.firstChild(table, OPERON.NS, "db-key");
		if(dbt==null) throw new IOException("op:db-key not found");
		structure.key= new DatabaseRecord(dbt,structure);
		dbt = XMLUtilities.firstChild(table, OPERON.NS, "db-value");
		if(dbt==null) throw new IOException("op:db-value not found");
		structure.value= new DatabaseRecord(dbt,structure);
		return structure;
		}
	
	protected SimpleField newField(Element e)
		{
		return new SimpleField(e);
		}
	
	private void parseField(Structure structure,Element root) throws IOException
		{
		SimpleField field= newField(root);
		field.mystructure=structure;
		structure.fields.addElement(field);
		if(structure.name2field.containsKey(field.getName()))
			{
			throw new IOException("field "+field.getName()+" defined twice");
			}
		structure.name2field.put(field.getName(), field);
		
		
		}
	
	/** all definition */
	protected Vector<DefinitionFile> definitionFiles= new Vector<DefinitionFile>();
	
	/** ouput directory */
	private String outputDir=null;
	
	/** velocity engine */
	private VelocityEngine engine=null;



	private CodeGenerator() throws Exception
		{
		 engine = new VelocityEngine();
			
	         engine.setProperty(VelocityEngine.RESOURCE_LOADER, "class");
	         engine.setProperty(
	 	            "class." + VelocityEngine.RESOURCE_LOADER + ".class",
	 	            ClasspathResourceLoader.class.getName()
	 	            );
	         engine.setProperty(VelocityEngine.VM_LIBRARY,
	        		 "/velocity/macros.vm"
	        		 );

	        engine.init();
		}
	
	private void merge(String templateName,VelocityContext context,File f)
		throws Exception
		{
		Template template =   this.engine.getTemplate("/velocity/"+templateName);
	
		PrintWriter out= new PrintWriter(f,"UTF-8");
		template.merge(context, out);
		out.flush();
		out.close();
		}
	


	public void makeJavaCode() throws Exception
		{
		VelocityContext context=null;
		File coreDir=new File(JOperonDir(),"core");
		coreDir.mkdir();
		File entitiesDir=new File(coreDir,"entities");
		entitiesDir.mkdir();
		File baseEntitiesDir=new File(entitiesDir,"base");
		baseEntitiesDir.mkdir();
		File jspDir= new File(JOperonDir(),"webapp/pages/jsp/list");
		jspDir.mkdir();

		for(DefinitionFile def: this.definitionFiles)
			{
			
			
			for(Structure struct: def.getStructures())
				{
				File builderDir= new File(baseEntitiesDir,"query4"+struct.getName());
				builderDir.mkdir();
				

				//generate the protected BaseXXX.java
				context= newContext();
				context.put("struct",struct);
				merge("entity.java.vm",context,new File(baseEntitiesDir,"Base"+struct.getName()+".java"));
				
				//generate the XXX.jaav derived from BaseXXX.java if needed
				File topClass= new File(entitiesDir,struct.getName()+".java");
				if(!topClass.exists())
					{
					context= newContext();
					context.put("struct",struct);
					merge("topclass.java.vm",context,topClass);
					}
				
				//generate the XXXQueryBuilder.java
				context= newContext();
				context.put("struct",struct);
				merge("queryTree.jj.vm",context,new File(builderDir,struct.getName()+"QueryBuilder.jj"));


				//generate the basic JSP
				File jspFile= new File(jspDir,"list-"+struct.getName().toLowerCase()+".jsp");
				if(!jspFile.exists())
					{
					context= newContext();
					context.put("struct",struct);
					merge("list-items.jsp.vm",context,jspFile);
					}
				}
			
			
			
			context= newContext();
			context.put("def",def);
			merge("ant.build.vm",context,new File((File)null,"_build-entities.xml"));
			}
		
		}
		

			
	public void makeCCode() throws Exception
		{
		VelocityContext context=null;
		for(DefinitionFile def: this.definitionFiles)
			{
			/** generate the structure files */
			for(Structure struct: def.getStructures())
				{
				//generate the XXX.h
				context= newContext();
				context.put("struct",struct);
				merge("XXXstruct.h.vm",context,new File(coreDir(),struct.getName()+".h"));

				//generate the XXX.c
				context= newContext();
				context.put("struct",struct);
				merge("XXXstruct.c.vm",context,new File(coreDir(),struct.getName()+".c"));

				//generate the XXX_array.h
				context= newContext();
				context.put("struct",struct);
				merge("XXXstruct_array.h.vm",context,new File(coreDir(),struct.getName()+"_array.h"));
									
				//generate the XXX_array.c
				context= newContext();
				context.put("struct",struct);
				merge("XXXstruct_array.c.vm",context,new File(coreDir(),struct.getName()+"_array.c"));			

				//generate the XXXstruct_xmlread.h
				context= newContext();
				context.put("struct",struct);
				merge("XXXstruct_xmlread.h.vm",context,new File(coreDir(),struct.getName()+"_xmlread.h"));
				//generate the XXXstruct_xmlread.c
				context= newContext();
				context.put("struct",struct);
				merge("XXXstruct_xmlread.c.vm",context,new File(coreDir(),struct.getName()+"_xmlread.h"));

				//generate the bison file
				context= newContext();
				context.put("struct",struct);
				merge(  "yacc.vm", context,new File(coreDir(),struct.getName()+"_scanner.y"));

				 //generate the flex file
				context= newContext();
				context.put("struct",struct);
				merge(  "lex.vm", context,new File(coreDir(),struct.getName()+"_lexer.l"));	
				}
			
			/** generate the database files */
			for(DatabaseInstance db:def.getDatabases())
				{
				context= newContext();
				context.put( "struct", db.getStructure());
			
				 //generate the query C query file
				context= newContext();
				context.put("db",db);
				merge( "XXXstruct_select.c.vm", context,new File(coreDir(),db.getName()+"_dump.c"));

				 //generate to mysql C file
				context= newContext();
				context.put("db",db);
				merge( "XXXstruct2mysql.c.vm", context,new File(coreDir(),db.getName()+"2mysql.c"));
				
				//generate  C file for this database
				context= newContext();
				context.put("db",db);
				merge( "database.c.vm", context,new File(coreDir(),db.getName()+".c"));
				//generate  H file for this database
				context= newContext();
				context.put("db",db);
				merge( "database.h.vm", context,new File(coreDir(),db.getName()+".h"));
				}
			/** generate the operons files */
			for(Operon op: def.getOperons())
				{
				context= newContext();
				context.put( "op", op);
				merge( "operon.h.vm", context,new File(coreDir(),"op_"+op.getName()+".h"));
				
				}
			}
		/** make makefile */
		context = newContext();
		context.put( "deffiles", this.definitionFiles);
		merge(  "Makefile.vm",
			context,
			new File(coreDir(),"Makefile")
			);
			
		}
	
	private VelocityContext newContext() throws Exception
		{
		VelocityContext context =  new VelocityContext();
		context.put( "today", new java.util.Date());
		return context;
		}

	private File coreDir()
		{
		return new File(getOutputDir(), "core");
		}	
	private File JOperonDir()
		{
		return new File(getOutputDir(),"src/java/fr/cephb/joperon");
		}

	public void setOutputDir(String outputDir) {
		this.outputDir = outputDir;
		}
	
	public String getOutputDir()
		{
		return outputDir;
		}
	
	
	/** parse the XML file */
	public void parse(File xml) throws IOException
		{
		try
			{
			DocumentBuilderFactory f= DocumentBuilderFactory.newInstance();
			f.setNamespaceAware(true);
			f.setCoalescing(true);
			f.setExpandEntityReferences(true);
			f.setNamespaceAware(true);
			f.setValidating(false);
			f.setIgnoringComments(true);
			f.setIgnoringElementContentWhitespace(false);
			DocumentBuilder b=f.newDocumentBuilder();
			Document doc=b.parse(xml);
			Element root=doc.getDocumentElement();
			if(root==null) throw new IOException("No root");
			if(!(	root.getLocalName().equals("operon") &&
					OPERON.NS.equals(root.getNamespaceURI())
				)) throw new IOException("not a op:operon root");
			
			DefinitionFile file= new DefinitionFile(xml,root);
			this.definitionFiles.add(file);
			
			for(Element table:XMLUtilities.elements(root, OPERON.NS, "table"))
				{
				Structure s=parseTable(table);
				file.structures.addElement(s);
				}
			
			for(Element dbi:XMLUtilities.elements(root, OPERON.NS, "database"))
				{
				DatabaseInstance db= new DatabaseInstance(dbi,file);
				file.dbInstances.addElement(db);
				}
			for(Element dbi:XMLUtilities.elements(root, OPERON.NS, "environment"))
				{
				Operon op= new Operon(dbi,file);
				file.operons.addElement(op);
				}
			}
		catch(IOException err)
			{
			throw err;
			}
		catch(Throwable err)
			{
			throw new IOException(err);
			}
		}
	
	
	/**
 	 * parse the argumenrs on the command line
         *
         */
	public static void main(String[] args)
		{
		try {
			CodeGenerator app= new CodeGenerator();
			String lang="C";
			int optind=0;
			while(optind<args.length)
				{
				if(args[optind].equals("-h"))
					{
					System.err.println("Pierre Lindenbaum PhD. C code generator for Berkeley");
					System.err.println("-d base directory");
					System.err.println("-C generate code for C (default)");
					System.err.println("-J generate code for Java");
					System.err.println("-h this screen");
					return;
					}
				else if (args[optind].equals("-d"))
				     {
				     app.setOutputDir(args[++optind]);
				     }
				else if (args[optind].equals("-C"))
				     {
				     lang="C";
				     }
				else if (args[optind].equals("-J"))
				     {
				     lang="Java";
				     }
				 else if (args[optind].equals("--"))
				     {
				     ++optind;
				     break;
				     }
				else if (args[optind].startsWith("-"))
				     {
				     System.err.println("bad argument " + args[optind]);
				     System.exit(-1);
				     }
				else
				     {
				     break;
				     }
				++optind;
				}
			
			if(optind==args.length) return ;
			while(optind< args.length)
				{
				app.parse(new File(args[optind++]));
				}
			if(lang.equals("C"))
				{
				app.makeCCode();
				}
			else
				{
				app.makeJavaCode();
				}
			} 
		catch (Exception e)
			{
			e.printStackTrace();
			}
		}
	}
